using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using AutoMapper;
using Telerik.Web.Mvc.Extensions;
using TelerikMvcGridCustomBindingHelper.Exceptions;
using TelerikMvcGridCustomBindingHelper.Mapper;
using TelerikMvcGridCustomBindingHelper.TinyCache;
using TelerikMvcGridCustomBindingHelper.Util;

namespace TelerikMvcGridCustomBindingHelper.Projections
{
    public class ProjectionsEngine
    {
        private readonly ITinyCache<string> _cache;
        private object _projectionsOptions;

        public ProjectionsEngine()
        {
            _cache = GridModelMapper.ConfigurationObject.TinyCache;
        }

        public Expression<Func<TSource, TDestination>> CreateMapExpressionTo<TSource, TDestination>()
        {
            var mapExpressionsToCache = _cache.GetOrSetItem("mapExpressionsToCacheKey-{f4c084c8-aabb-4a2b-981f-a790ec62c336}",
                                () => new ConcurrentDictionary<TypePair, LambdaExpression>());

            return (Expression<Func<TSource, TDestination>>)
                mapExpressionsToCache.GetOrAdd(new TypePair(typeof(TSource), typeof(TDestination)),
                tp => CreateMapExpressionTo(tp.SourceType, tp.DestinationType));
        }

        public Expression<Func<TSource, TSource>> CreateMapExpressionFrom<TSource, TDestination>()
        {
            var mapExpressionsFromCache = _cache.GetOrSetItem("mapExpressionsFromCache-{dfcdc6cc-98f3-4dc1-a71a-36ca3861e897}",
                                () => new ConcurrentDictionary<TypePair, LambdaExpression>());

            return (Expression<Func<TSource, TSource>>)
                mapExpressionsFromCache.GetOrAdd(new TypePair(typeof(TSource), typeof(TDestination)),
                tp => CreateMapExpressionFrom(tp.SourceType, tp.DestinationType));
        }

        public HashSet<string> PropertiesIntersection<TSource, TDestination>()
        {
            var propertiesIntersectionCache = _cache.GetOrSetItem("propertiesIntersectionCacheKey-{a225ec20-ac2b-4c8a-8ecc-f3f0762610b3}",
                                () => new ConcurrentDictionary<TypePair, HashSet<string>>());

            return propertiesIntersectionCache.GetOrAdd(new TypePair(typeof(TSource), typeof(TDestination)),
                tp => PropertiesIntersection<TSource>(typeof(TSource), typeof(TDestination)));
        }

        //todo: merge with last AutoMapper updates in the codebase
        private static LambdaExpression CreateMapExpressionTo(Type typeIn, Type typeOut)
        {
            Guard.IsMapped(typeIn, typeOut);

            var typeMap = AutoMapper.Mapper.FindTypeMapFor(typeIn, typeOut);

            // this is the input parameter of this expression with name <variableName>
            var instanceParameter = Expression.Parameter(typeIn);

            var bindings = new List<MemberBinding>();
            foreach (var propertyMap in typeMap.GetPropertyMaps())
            {
                var destinationMember = propertyMap.DestinationProperty.MemberInfo;

                Expression currentChild = instanceParameter;
                var currentChildType = typeIn;
                foreach (var resolver in propertyMap.GetSourceValueResolvers())
                {
                    var getter = resolver.As<IMemberGetter>();
                    if (getter != null)
                    {
                        var memberInfo = getter.MemberInfo;

                        var propertyInfo = memberInfo.As<PropertyInfo>();
                        if (propertyInfo != null)
                        {
                            currentChild = Expression.Property(currentChild, propertyInfo);
                            currentChildType = propertyInfo.PropertyType;
                        }

                        var fieldInfo = memberInfo.As<FieldInfo>();
                        if (fieldInfo != null)
                        {
                            currentChild = Expression.Field(currentChild, fieldInfo);
                            currentChildType = fieldInfo.FieldType;
                        }
                    }
                    else
                    {
                        var oldParameter = propertyMap.CustomExpression.Parameters.Single();
                        var newParameter = instanceParameter;
                        var extractMembersVisitor = new ExtractMembersVisitor(newParameter, oldParameter);
                        extractMembersVisitor.Visit(propertyMap.CustomExpression.Body);
                        currentChild = extractMembersVisitor.MemberExpressions.FirstOrDefault();
                        currentChildType = Reflector.GetMemberType(propertyMap.SourceMember ?? extractMembersVisitor.MemberInfos.FirstOrDefault());
                    }
                }

                var memberType = Reflector.GetMemberType(destinationMember);

                // next to lists, also arrays
                // and objects!!!
                if (memberType != null &&
                    memberType.GetInterface("IEnumerable", true) != null &&
                    memberType != typeof(string))
                {
                    var destinationListType = memberType.GetGenericArguments().First();

                    // is list
                    var sourceListType = currentChildType.GetGenericArguments().First();

                    //var newVariableName = "t" + (i++);
                    var transformedExpression = CreateMapExpressionTo(sourceListType, destinationListType);

                    var selectExpression = Expression.Call(
                        typeof(Enumerable),
                        "Select",
                        new[] { sourceListType, destinationListType },
                        currentChild,
                        transformedExpression);

                    var toListCallExpression = Expression.Call(
                        typeof(Enumerable),
                        "ToList",
                        new[] { destinationListType },
                        selectExpression);

                    // todo .ToArray()
                    bindings.Add(Expression.Bind(destinationMember, toListCallExpression));
                }
                else
                {
                    // does of course not work for subclasses etc./generic ...
                    if (memberType != null && (currentChildType != memberType &&
                        // avoid nullable etc.
                        memberType.BaseType != typeof(ValueType) &&
                        memberType.BaseType != typeof(Enum)))
                    {
                        if (AutoMapper.Mapper.FindTypeMapFor(currentChildType, memberType) == null)
                        {
                            string errormsg;

                            if (propertyMap.SourceMember != null)
                                errormsg = string.Format("Projection Error: mapping not found for property: \"{0}.{1} [{2}]\" to \"{3}.{4} [{5}]\".",
                                    typeIn, propertyMap.SourceMember.Name, Reflector.GetMemberType(propertyMap.SourceMember), typeOut, memberType.Name, memberType);
                            else
                                errormsg = string.Format("Projection Error: not supported expression: \"{0}\" to \"{1}.{2} [{3}]\".",
                                        propertyMap.CustomExpression.ToString().Replace("src", typeIn.ToString()), typeOut, memberType.Name, memberType);

                            throw new NotSupportedException(errormsg);
                        }

                        var transformedExpression = CreateMapExpressionTo(currentChildType, memberType);

                        var expr = Expression.Invoke(transformedExpression, currentChild);

                        bindings.Add(Expression.Bind(destinationMember, expr));
                    }
                    else if (currentChild != null)
                    {
                        if (currentChild.NodeType == ExpressionType.Convert)
                        {
                            var unaryExpression = currentChild.As<UnaryExpression>();
                            if (memberType != null && unaryExpression != null
                                && currentChild.Type != memberType)
                            {
                                currentChild = Expression.Convert(unaryExpression.Operand, memberType);
                            }
                            else throw new NotSupportedException(String.Format("{0} {1}", currentChild.GetType(), currentChild.NodeType));
                        }

                        bindings.Add(Expression.Bind(destinationMember, currentChild));
                    }
                }

            }

            var total = Expression.MemberInit(Expression.New(typeOut), bindings.ToArray());

            return Expression.Lambda(total, instanceParameter);
        }

        private static LambdaExpression CreateMapExpressionFrom(Type typeIn, Type typeOut)
        {
            Guard.IsMapped(typeIn, typeOut);

            var typeMap = AutoMapper.Mapper.FindTypeMapFor(typeIn, typeOut);

            // this is the input parameter of this expression with name <variableName>
            var instanceParameter = Expression.Parameter(typeIn);

            var bindings = new List<MemberBinding>();
            foreach (var propertyMap in typeMap.GetPropertyMaps())
            {
                var sourceMember = propertyMap.SourceMember;
                var destinationMember = propertyMap.DestinationProperty.MemberInfo;

                Expression currentChild = instanceParameter;
                var currentChildType = typeIn;
                foreach (var resolver in propertyMap.GetSourceValueResolvers())
                {
                    var getter = resolver.As<IMemberGetter>();
                    if (getter != null)
                    {
                        var memberInfo = getter.MemberInfo;

                        var propertyInfo = memberInfo.As<PropertyInfo>();
                        if (propertyInfo != null)
                        {
                            currentChild = Expression.Property(currentChild, propertyInfo);
                            currentChildType = propertyInfo.PropertyType;
                        }

                        var fieldInfo = memberInfo.As<FieldInfo>();
                        if (fieldInfo != null)
                        {
                            currentChild = Expression.Field(currentChild, fieldInfo);
                            currentChildType = fieldInfo.FieldType;
                        }
                    }
                    else
                    {
                        var oldParameter = propertyMap.CustomExpression.Parameters.Single();
                        var newParameter = instanceParameter;
                        var extractMembersVisitor = new ExtractMembersVisitor(newParameter, oldParameter);

                        var visitedExpression = extractMembersVisitor.Visit(propertyMap.CustomExpression.Body);
                        currentChild = extractMembersVisitor.MemberExpressions.FirstOrDefault() ?? visitedExpression;

                        currentChildType = Reflector.GetMemberType(sourceMember);
                    }
                }

                var sourceMemberType = Reflector.GetMemberType(sourceMember);
                var destMemberType = Reflector.GetMemberType(destinationMember);

                // next to lists, also arrays
                // and objects!!!
                if (sourceMemberType != null && destMemberType != null &&
                    sourceMemberType.GetInterface("IEnumerable", true) != null &&
                    sourceMemberType != typeof(string))
                {
                    var destinationListType = destMemberType.GetGenericArguments().First();

                    // is list
                    var sourceListType = currentChildType.GetGenericArguments().First();

                    //var newVariableName = "t" + (i++);
                    var transformedExpression = CreateMapExpressionFrom(sourceListType, destinationListType);

                    var selectExpression = Expression.Call(
                        typeof(Enumerable),
                        "Select",
                        new[] { sourceListType, sourceMemberType.GetGenericArguments().First() },
                        currentChild,
                        transformedExpression);

                    var toListCallExpression = Expression.Call(
                        typeof(Enumerable),
                        "ToList",
                        new[] { sourceMemberType.GetGenericArguments().First() },
                        selectExpression);

                    // todo .ToArray()
                    bindings.Add(Expression.Bind(sourceMember, toListCallExpression));
                }
                else
                {
                    // does of course not work for subclasses etc./generic ...
                    if (destMemberType != null && (sourceMemberType != null && (currentChildType != destMemberType &&
                        // avoid nullable etc.
                        sourceMemberType.BaseType != typeof(ValueType) &&
                        sourceMemberType.BaseType != typeof(Enum))))
                    {
                        var transformedExpression = CreateMapExpressionFrom(currentChildType, destMemberType);

                        var expr = Expression.Invoke(transformedExpression, currentChild);

                        bindings.Add(Expression.Bind(sourceMember, expr));
                    }
                    else
                    {
                        var propertyInfo = sourceMember.As<PropertyInfo>();
                        //propertyInfo = propertyInfo ?? currentChildType;

                        if (propertyInfo != null && 
                            (typeIn.IsAssignableFrom(propertyInfo.ReflectedType) || typeIn.IsSubclassOf(propertyInfo.ReflectedType)))
                        {
                            bindings.Add(Expression.Bind(sourceMember, currentChild));
                        }
                    }
                }
            }

            // Override with custom mappings
            var gridPropertyInfos = GridModelMapper.GetPropertyMaps(typeIn, typeOut);

            if (gridPropertyInfos != null)
            {
                foreach (var gridPropertyInfo in gridPropertyInfos)
                {
                    var info = gridPropertyInfo;
                    var firstOrDefault = bindings.FirstOrDefault(binding => binding.Member.Name == info.PropertyPath);

                    bindings.Remove(firstOrDefault);
                }

                foreach (var gridPropertyInfo in gridPropertyInfos)
                {
                    var propertyInfo = typeIn.GetProperty(gridPropertyInfo.PropertyPath);

                    if (propertyInfo != null)
                    {
                        bindings.Add(Expression.Bind(propertyInfo, Expression.Property(instanceParameter, propertyInfo)));
                    }

                    var fieldInfo = typeIn.GetField(gridPropertyInfo.PropertyPath);

                    if (fieldInfo != null)
                    {
                        bindings.Add(Expression.Bind(fieldInfo, Expression.Field(instanceParameter, fieldInfo)));
                    }
                }
            }

            if (bindings.Count <= 0)
            {
                throw new ProjectionsException(typeIn, typeOut);
            }

            var total = Expression.MemberInit(Expression.New(typeIn), bindings.ToArray());

            return Expression.Lambda(total, instanceParameter);
        }

        private static HashSet<string> PropertiesIntersection<TRoot>(Type typeIn, Type typeOut, string rootPath = "")
        {
            Guard.IsMapped(typeIn, typeOut);

            var typeMap = AutoMapper.Mapper.FindTypeMapFor(typeIn, typeOut);
            var bindings = new HashSet<string>();

            foreach (var propertyMap in typeMap.GetPropertyMaps())
            {
                var destinationMember = propertyMap.DestinationProperty.MemberInfo;
                if (destinationMember == null)
                    continue;

                var memberInfos = new List<MemberInfo>();
                if (propertyMap.SourceMember != null)
                {
                    memberInfos.Add(propertyMap.SourceMember);
                }
                else
                {
                    foreach (var getter in propertyMap.GetSourceValueResolvers().Select(resolver => resolver.As<IMemberGetter>()))
                    {
                        if (getter != null)
                        {
                            if (getter.MemberInfo != null)
                            {
                                memberInfos.Add(getter.MemberInfo);
                                break;
                            }
                        }
                        else
                        {
                            var oldParameter = propertyMap.CustomExpression.Parameters.First();
                            var newParameter = Expression.Parameter(typeIn);

                            var memberExtrator = new ExtractMembersVisitor(newParameter, oldParameter);
                            memberExtrator.Visit(propertyMap.CustomExpression.Body);
                            memberInfos = memberExtrator.MemberInfos.ToList();
                            break;
                        }
                    }
                }

                foreach (var memberInfo in memberInfos)
                {
                    var currentChildType = Reflector.GetMemberType(memberInfo);
                    var propertyPath = !string.IsNullOrWhiteSpace(rootPath) ? string.Concat(rootPath, ".", memberInfo.Name) : memberInfo.Name;

                    var destMemberType = Reflector.GetMemberType(destinationMember);
                    if (destMemberType == null) continue;
                    // next to lists, also arrays and objects!!!
                    if (destMemberType.GetInterface("IEnumerable", true) != null && destMemberType != typeof(string))
                    {
                        bindings.Add(propertyPath);
                    }
                    else
                    {
                        // does of course not work for subclasses etc./generic ...
                        if (currentChildType != destMemberType &&
                            destMemberType.BaseType != typeof(ValueType) && destMemberType.BaseType != typeof(Enum)
                            && AutoMapper.Mapper.FindTypeMapFor(currentChildType, destMemberType) != null)
                        {
                            bindings.AddRange(PropertiesIntersection<TRoot>(currentChildType, destMemberType, propertyPath));
                        }
                        else
                        {
                            bindings.Add(propertyPath);
                        }
                    }
                }
            }

            // Including GridModelMapper mappings
            var gridPropertyInfos = GridModelMapper.GetPropertyMaps(typeIn, typeOut);

            if (gridPropertyInfos != null)
            {
                foreach (var propertyPath in gridPropertyInfos.Where(gpi => gpi.PropertyPath.IsNotNullOrWhiteSpace()).Select(gpi => gpi.PropertyPath))
                {
                    var path = rootPath.IsNotNullOrWhiteSpace() ? string.Concat(rootPath, ".", propertyPath) : propertyPath;

                    MemberInfo property = typeIn.GetProperty(path);
                    property = property ?? typeIn.GetField(path);
                    var isInnerType = false;

                    if (property != null)
                    {
                        if (property.MemberType == MemberTypes.Property)
                        {
                            isInnerType = ((PropertyInfo)property).PropertyType.GetProperties().Any();
                        }
                        else if (property.MemberType == MemberTypes.Field)
                        {
                            isInnerType = ((FieldInfo)property).FieldType.GetProperties().Any();
                        }
                    }

                    if (!bindings.Contains(path) && path.IsNotNullOrWhiteSpace() && isInnerType == false)
                        bindings.Add(path);
                }
            }

            return bindings;
        }

        #region Dynamic projections

        internal DynamicLinqSelector BuildSelector<TSource, TTarget>(ProjectionsOptionsImpl<TSource> options)
            where TSource : class
            where TTarget : class
        {
            _projectionsOptions = options;

            var cache = _cache.GetOrSetItem("selectorCacheKey-{9bec58f7-2e99-4f44-be55-073df25265a8}",
                                () => new ConcurrentDictionary<TypePair, DynamicLinqSelector>());

            return cache.GetOrAdd(new TypePair(typeof(TSource), typeof(TTarget), options),
                tp =>
                {
                    var propertyMaps = BuildPropertyMaps<TSource, TTarget>(options);
                    return new DynamicLinqSelector(propertyMaps);
                });
        }

        private Dictionary<string, string> BuildPropertyMaps<TSource, TTarget>(ProjectionsOptions<TSource> options)
            where TSource : class
            where TTarget : class
        {
            _projectionsOptions = options;

            var visitor = new PropertyPathVisitor();

            if (options != null)
            {
                if (options.PropertiesToUse.Any())
                {
                    var propMapsToUse = new Dictionary<string, string>();
                    foreach (var expression in options.PropertiesToUse)
                    {
                        visitor.Visit(expression);
                        propMapsToUse.AddRange(BuildPropertyMaps<TSource>(visitor.Properties));
                        visitor.Reset();
                    }

                    return propMapsToUse;
                }

                var propMapsToInclude = new Dictionary<string, string>();
                if (options.PropertiesToInclude.Any())
                {
                    foreach (var expression in options.PropertiesToInclude)
                    {
                        visitor.Visit(expression);
                        propMapsToInclude.AddRange(BuildPropertyMaps<TSource>(visitor.Properties));
                        visitor.Reset();
                    }
                }

                var propMapsToIgnore = new Dictionary<string, string>();
                if (options.PropertiesToIgnore.Any())
                {
                    foreach (var expression in options.PropertiesToIgnore)
                    {
                        visitor.Visit(expression);
                        propMapsToIgnore.AddRange(BuildPropertyMaps<TSource>(visitor.Properties));
                        visitor.Reset();
                    }
                }

                var props = PropertiesIntersection<TSource, TTarget>();
                var propertyMapsFromCache = BuildPropertyMaps<TSource>(props);

                propMapsToInclude = propertyMapsFromCache.Concat(
                    propMapsToInclude.Where(pair => !propertyMapsFromCache.ContainsKey(pair.Key)))
                    .ToDictionary(pair => pair.Key, pair => pair.Value);

                propMapsToInclude = propMapsToInclude.Except(propMapsToIgnore)
                    .ToDictionary(pair => pair.Key, pair => pair.Value);

                return propMapsToInclude;
            }

            var properties = PropertiesIntersection<TSource, TTarget>();
            return BuildPropertyMaps<TSource>(properties);
        }

        private static Dictionary<string, string> BuildPropertyMaps<TSource>(IEnumerable<string> properties) where TSource : class
        {
            var propertyMaps = new Dictionary<string, string>();

            foreach (var property in properties.Distinct())
            {
                var prop = property;
                if (prop.Contains("."))
                {
                    prop = string.Format("{0}", prop.Replace(".", "_"));
                    while (typeof(TSource).GetProperty(prop) != null)
                    {
                        prop = string.Concat("_", prop);
                    }
                }
                propertyMaps.Add(property, prop);
            }

            return propertyMaps;
        }

        public List<TSource> GetData<TSource, TTarget>(IQueryable data)
            where TSource : class
            where TTarget : class
        {
            if (data.ElementType.BaseType == null || !data.ElementType.BaseType.IsAssignableFrom(typeof(DynamicClass)))
                throw new NotSupportedException("The underlying type must be assignable from System.Linq.Dynamic.DynamicClass.");

            var propmaps = BuildPropertyMaps<TSource, TTarget>(_projectionsOptions.As<ProjectionsOptions<TSource>>());

            var sources = new List<TSource>();
            foreach (var item in data)
            {
                var source = ObjectFactory<TSource>.Create();
                foreach (var propmap in propmaps)
                {
                    var value = Reflector.GetPropertyValue(item, propmap.Value);
                    Reflector.SetPropertyValue(source, propmap.Key, value);
                }
                sources.Add(source);
            }

            return sources;
        }

        #endregion
    }

    public class DynamicLinqSelector
    {
        public Dictionary<string, string> PropertyMaps { get; private set; }
        public string Expression { get; private set; }

        public DynamicLinqSelector(Dictionary<string, string> propertyMaps)
        {
            PropertyMaps = propertyMaps;
            Expression = BuildSelectExpression(propertyMaps);
        }

        private static string BuildSelectExpression(Dictionary<string, string> propmaps)
        {
            var stringBuilder = new StringBuilder("new(");

            foreach (var propmap in propmaps)
            {
                if (propmap.Key.Equals(propmap.Value))
                {
                    stringBuilder.AppendFormat("{0}, ", propmap.Key);
                }
                else
                {
                    stringBuilder.AppendFormat("{0} as {1}, ", propmap.Key, propmap.Value);
                }
            }

            var selector = stringBuilder.ToString();

            if (selector.EndsWith(", "))
            {
                selector = selector.Remove(selector.Length - 2);
            }

            selector = string.Concat(selector, ")");

            return selector;
        }
    }
}